var uglify = require("uglify-js"),
    fs = require("fs"),
    p = require("path"),
    zlib = require("zlib"),
    files = [
        "lzma_worker.js",
        "lzma-c.js",
        "lzma-d.js",
        "lzma.js",
    ],
    minify_props_files = [
         "lzma_worker.js",
         "lzma-c.js",
         "lzma-d.js",
    ],
    params = get_params(),
    text_output = "",
    stats = {};

function get_params(argv)
{
    var i,
        params = {};
    
    argv = argv || process.argv;
    
    for (i = process.argv.length - 1; i >= 2; i -= 1) {
        params[process.argv[i].replace(/^-+/, "")] = 1;
    }
    
    return params;
}

function log(str)
{
    text_output += str + "\n";
    console.log(str);
}

function filesize(path, cb)
{
    return fs.statSync(path).size;
}

function sort_obj(obj)
{
    return Object.keys(obj).sort(function sorter(a, b)
    {
        return obj[b] - obj[a];
    });
}

function calculate_minify_value(props)
{
    Object.keys(props).forEach(function oneach(prop)
    {
        props[prop] = prop.length * props[prop];
    });
}

function base54_64(arr)
{
    var chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_$0123456789",
        i,
        len = arr.length,
        new_names = [],
        str,
        pos,
        base;
    
    for (i = 0; i < len; i += 1) {
        pos = i;
        str = "";
        base = 54;
        do {
            ///NOTE: This is not perfect. It skips the first char (a) on the last place when more than one letter.
            str += chars[pos % base];
            pos = Math.floor(pos / base);
            /// After the first character, we can use numbers.
            base = 64;
        } while (pos > 0);
        new_names[i] = str;
    }
    
    return new_names;
}

function minify_properties(code)
{
    var props = {},
        ignore = ["LZMA_WORKER", "on_progress", "on_finish"],
        sorted_props,
        new_names,
        prop_regex = /\.([_$a-zA-Z]*[_$][_$a-zA-Z0-9]*|kFixHashSize|kNumHashDirectBytes|kMinMatchCheck|outBytesProcessed|decoder|encoder|nowPos64|outSize|alive|processedInSize|finished|inBytesProcessed|processedInSize|[Ss]tate|prevByte|Prev1IsChar|Prev2|BackPrev|PosPrev|Models|backRes|properties|processedOutSize|tempPrices|backRes|repLens|Price|Backs[0123]|NumBitLevels|Range|Stream|explicitLength|count|pos|buf|chunker|rep[0-3s]|Code|Low|data|mode|output)/g;
    
    /// We want to replace propeters that have an underscore or a dollar sign.
    code.replace(prop_regex, function calc(prop)
    {
        prop = prop.substr(1); /// Remove the perios.
        if (ignore.indexOf(prop) === -1) {
            if (!props[prop]) {
                props[prop] = 1;
            } else {
                props[prop] += 1;
            }
        }
    });
    
    calculate_minify_value(props);
    sorted_props = sort_obj(props);
    
    new_names = base54_64(sorted_props);
    
    code = code.replace(prop_regex, function calc(prop)
    {
        var index,
            partial_prop = prop.substr(1); /// Remove the perios.
        
        index = sorted_props.indexOf(partial_prop);
        
        if (index > -1) {
            return "." + new_names[index];
        }
        return prop;
    });
    
    return code;
}

function split_lzma_worker()
{
    var data = fs.readFileSync(p.join(__dirname, "src", "lzma_worker.js"), "utf8"),
        c,
        d,
        notice = "///NOTE: This file was generated by minify.js from lzma_worker.js. Do not modify.\n\n";
    
    /// Delete both only code.
    data = data.replace(/\/\*\* xs \*\/[\S\s]*?\/\*\* xe \*\//g, "");
    
    /// Delete decompress code.
    c = data.replace(/\/\*\* ds \*\/[\S\s]*?\/\*\* de \*\//g, "");
    
    /// Enable compress only code.
    c = c.replace(/\/\/\/ co:/g, "");
    
    /// Delete decompress code.
    d = data.replace(/\/\*\* cs \*\/[\S\s]*?\/\*\* ce \*\//g, "");
    
    /// Enable compress only code.
    d = d.replace(/\/\/\/ do:/g, "");
    
    fs.writeFileSync(p.join(__dirname, "src", "lzma-c.js"), notice + c);
    fs.writeFileSync(p.join(__dirname, "src", "lzma-d.js"), notice + d);
}

function get_kb(bytes)
{
    return (bytes / 1024).toFixed(bytes < 1024 ? 2 : 1) + " KB";
}

function pad(str, len)
{
    return str.length >= len ? str : new Array(len - str.length + 1).join(" ") + str;
}

function replace_data(str, filename)
{
    return str.replace(new RegExp("(" + filename + "\\s*\\|\\s*\\S+\\s*\\|)([^|]+)\\|([^|]+)"), function replace(all, base, min, gzip)
    {
        var new_min  = pad(get_kb(stats[filename].min)  + " ", min.length),
            new_gzip = pad(get_kb(stats[filename].gzip) + " ", gzip.length);
        
        return base + new_min + "|" + new_gzip;
    });
}

function update_readme()
{
    var data,
        readme_path = p.join(__dirname, "readme.md");
    
    data = fs.readFileSync(readme_path, "utf8");
    
    data = replace_data(data, "lzma_worker.js");
    data = replace_data(data, "lzma-c.js");
    data = replace_data(data, "lzma-d.js");
    
    fs.writeFileSync(readme_path, data);
}

function staged_files_found(cb)
{
    require("child_process").exec("git diff-index --quiet --cached HEAD", function onexec(err)
    {
        if (err) {
            if (err.code === 1) {
                return cb(true);
            }
            throw err;
        }
        cb(false);
    });
}


function commit(cb)
{
    var execFile = require("child_process").execFile;
    
    function ret()
    {
        if (cb) {
            cb();
        }
    }
    
    execFile("git", ["add", "readme.md"], function onexec(err)
    {
        if (err) {
            throw err;
        }
        execFile("git", ["add", "src"], function onexec(err)
        {
            if (err) {
                throw err;
            }
            
            staged_files_found(function oncheck(found)
            {
                if (!found) {
                    console.log("No files need updating.");
                    return ret();
                }
                execFile("git", ["commit", "-m", "Minified:\n\n" + text_output.trim()], function onexec(err)
                {
                    if (err) {
                        throw err;
                    }
                    
                    ret();
                });
            });
        });
    });
}

function minify()
{
    split_lzma_worker();
    
    (function loop(i)
    {
        var file,
            full_path,
            min_path,
            result,
            orig_size,
            min_size,
            gzmin_size,
            ext;
        
        if (i < files.length) {
            file = files[i];
            ext = p.extname(file);
            full_path = p.join(__dirname, "src", file);
            min_path = p.join(__dirname, "src", p.basename(file, ext) + "-min" + ext);
            orig_size = filesize(full_path);
            
            log(file);
            stats[file] = {};
            
            result = uglify.minify(full_path, {
                mangle: {
                    sort:     false, /// As FALSE, the plain JS is bigger, but gzipped is smaller!
                    toplevel: true,
                },
                compress: {
                    sequences:    true,
                    dead_code:    true,
                    conditionals: true,
                    booleans:     true,
                    unused:       true,
                    loops:        true,
                    if_return:    true,
                    join_vars:    true,
                    pure_getters: true,
                    cascade:      true,
                    join_vars:    true,
                    evaluate:     true,
                    comparisons:  true,
                    properties:   true,
                    negate_iife:  true,
                    keep_fargs:   false,
                    hoist_vars:   false, /// As FALSE, the plain JS is bigger, but gzipped is smaller!
                    hoist_funs:   true,  /// As TRUE, the plain JS is bigger, but gzipped is smaller!
                    warnings:     true,
                    unsafe:       true,
                },
                output: {
                    comments: /^!|@preserve|@license|@cc_on/i,
                },
            });
            
            if (minify_props_files.indexOf(file) > -1) {
                result.code = minify_properties(result.code);
            }
            
            fs.writeFileSync(min_path, result.code);
            
            min_size = filesize(min_path);
            
            stats[file].min = min_size;
            
            log("Original size: " + orig_size + " bytes (" + get_kb(orig_size) + ")");
            log("Minified size: " + min_size + " bytes (" + get_kb(min_size) + ")");
            log("Compression:   " + (orig_size / min_size).toFixed(4) + " x smaller");
            
            zlib.gzip(result.code, function(err, buffer) {
                if (err) {
                    throw err;
                }
                
                gzmin_size = buffer.length;
                
                stats[file].gzip = gzmin_size;
                
                ///NOTE: We could write the file if we wanted to like this: fs.writeFileSync(p.join(__dirname, "src", p.basename(file, ext) + "-min" + ext + ".gz"), buffer);
                log("Gzipped size:  " + gzmin_size + " bytes (" + get_kb(gzmin_size) + ")");
                log("Gzipped ratio: " + (orig_size / gzmin_size).toFixed(4) + " x smaller");
                log("");
                loop(i + 1);
            });
        } else if (params.save) {
            console.log("Updating Readme");
            update_readme();
            console.log("Committing");
            commit();
        }
    }(0));
}

if (params.help) {
    console.log("");
    console.log("Usage: node minify.js [FLAGS]");
    console.log("");
    console.log("  --save  Update readme.md and commit.");
    console.log("");
    process.exit();
}

if (params.save) {
    staged_files_found(function oncheck(found)
    {
        if (found) {
            console.log("Found staged files. Commit first.");
            process.exit(1);
        }
        minify();
    });
} else {
    minify();
}
